package com.base.data.elasticsearch.core;

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.base.data.elasticsearch.ElasticsearchException;
import com.base.data.elasticsearch.annotations.Document;
import com.base.data.elasticsearch.annotations.ScriptedField;
import com.base.data.elasticsearch.core.aggregation.AggregatedPage;
import com.base.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import com.base.data.elasticsearch.core.mapping.BaseElasticsearchPersistentEntity;
import com.base.data.elasticsearch.core.mapping.BaseElasticsearchPersistentProperty;
import com.base.data.elasticsearch.util.RefectUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.get.MultiGetItemResponse;
import org.elasticsearch.action.get.MultiGetResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHitField;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mapping.context.MappingContext;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

@Slf4j
public class BaseDefaultResultMapper extends BaseAbstractResultMapper {

    private MappingContext<? extends BaseElasticsearchPersistentEntity<?>, BaseElasticsearchPersistentProperty> mappingContext;

    public BaseDefaultResultMapper() {
        super(new BaseDefaultEntityMapper());
    }

    public BaseDefaultResultMapper(MappingContext<? extends BaseElasticsearchPersistentEntity<?>, BaseElasticsearchPersistentProperty> mappingContext) {
        super(new BaseDefaultEntityMapper());
        this.mappingContext = mappingContext;
    }

    public BaseDefaultResultMapper(BaseEntityMapper entityMapper) {
        super(entityMapper);
    }

    public BaseDefaultResultMapper(
            MappingContext<? extends BaseElasticsearchPersistentEntity<?>, BaseElasticsearchPersistentProperty> mappingContext,
            BaseEntityMapper entityMapper) {
        super(entityMapper);
        this.mappingContext = mappingContext;
    }

    @Override
    public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
        long totalHits = response.getHits().totalHits();
        List<T> results = new ArrayList<>();
        long countMapEntity = 0L;
        for (SearchHit hit : response.getHits()) {
            if (hit != null) {
                T result = null;
                long start = System.nanoTime();
                if (Strings.isNotBlank(hit.getSourceAsString())) {
                    result = mapEntity(hit.getSourceAsString(), clazz);
                } else {
                    result = mapEntity(hit.getFields().values(), clazz);
                }
                countMapEntity += (System.nanoTime()-start);
                setPersistentEntityId(result, hit.getId(), clazz);
                populateScriptFields(result, hit);
                results.add(result);
            }
        }
        log.debug("countMapEntity:{}",countMapEntity/1000_000);
        return new AggregatedPageImpl<T>(results, pageable, totalHits, response.getAggregations(), response.getScrollId());
    }

    private <T> void populateScriptFields(T result, SearchHit hit) {
        if (hit.getFields() != null && !hit.getFields().isEmpty() && result != null) {
            for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) {
                ScriptedField scriptedField = field.getAnnotation(ScriptedField.class);
                if (scriptedField != null) {
                    String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name();
                    SearchHitField searchHitField = hit.getFields().get(name);
                    if (searchHitField != null) {
                        field.setAccessible(true);
                        try {
                            field.set(result, searchHitField.getValue());
                        } catch (IllegalArgumentException e) {
                            throw new ElasticsearchException("failed to set scripted field: " + name + " with value: "
                                    + searchHitField.getValue(), e);
                        } catch (IllegalAccessException e) {
                            throw new ElasticsearchException("failed to access scripted field: " + name, e);
                        }
                    }
                }
            }
        }
    }

    private <T> T mapEntity(Collection<SearchHitField> values, Class<T> clazz) {
        return mapEntity(buildJSONFromFields(values), clazz);
    }

    //to test
    private String buildJSONFromFields(Collection<SearchHitField> values) {
        JsonFactory nodeFactory = new JsonFactory();
        try {
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            JsonGenerator generator = nodeFactory.createGenerator(stream, JsonEncoding.UTF8);
            generator.writeStartObject();
            for (SearchHitField value : values) {
                if (value.getValues().size() > 1) {
                    generator.writeArrayFieldStart(value.getName());
                    for (Object val : value.getValues()) {
                        generator.writeObject(val);
                    }
                    generator.writeEndArray();
                } else {
                    generator.writeObjectField(value.getName(), value.getValue());
                }
            }
            generator.writeEndObject();
            generator.flush();
            return new String(stream.toByteArray(), Charset.forName("UTF-8"));
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    public <T> T mapResult(GetResponse response, Class<T> clazz) {
        T result = mapEntity(response.getSourceAsString(), clazz);
        if (result != null) {
            setPersistentEntityId(result, response.getId(), clazz);
        }
        return result;
    }

    @Override
    public <T> T mapResult(SearchResponse response, Class<T> clazz) {
        final SearchHit[] hits = response.getHits().getHits();
        if(null != hits && hits.length ==1) {
            final T t = mapEntity(hits[0].getSourceAsString(), clazz);
//            if (t != null) {
//                setPersistentEntityId(t, RefectUtil.refectIdValue(t,hits[0].getSourceAsString()), clazz);
//            }
            return t;
        }else {
            return null;
        }
    }

    @Override
    public <T> LinkedList<T> mapResults(SearchResponse response, Class<T> clazz) {
        LinkedList<T> list = new LinkedList<>();
        for (SearchHit hit : response.getHits()) {
            if (hit != null) {
                T result = null;
                if (Strings.isNotBlank(hit.getSourceAsString())) {
                    result = mapEntity(hit.getSourceAsString(), clazz);
                } else {
                    result = mapEntity(hit.getFields().values(), clazz);
                }
                setPersistentEntityId(result, hit.getId(), clazz);
                populateScriptFields(result, hit);
                list.add(result);
            }
        }
        return list;
    }

    private <T> void setPersistentEntityId(T result, String id, Class<T> clazz) {

        if (mappingContext != null && clazz.isAnnotationPresent(Document.class)) {

            BaseElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
            BaseElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();

            // Only deal with String because ES generated Ids are strings !
            if (idProperty != null && idProperty.getType().isAssignableFrom(String.class)) {
                persistentEntity.getPropertyAccessor(result).setProperty(idProperty, id);
            }

        }
    }
}
